AnEx Learning Summit
Thursday October 21, 2021

Sarah Rankin
Quantitative Data Analyst
Stategy and Public Impact
New York Public Library
sarahrankin@nypl.org

Topics

Setup

This is an Rmarkdown notebook. If you’re viewing it in a web browser, you can click on Code->Download Rmd in the top right corner, then open it in RStudio and to run/edit the code. If you don’t use RStudio you can also just copy the code into your editor of choice.

We’ll be using a variety of tidyverse packages as well as sf and leaflet. If you don’t have these packages installed, uncomment the following code and install them now. (It’ll take a little while.)

# install.packages("tidyverse")
# install.packages("sf")
# install.packages("leaflet")
#load just the tidyverse for now
library(tidyverse)

Quick ggplot refresher

Let’s create some very simple point data.

five_points <- data.frame(var1 = 1:5, var2 = as.integer(c(3,5,4,4,1)))
five_points 
five_points %>% 
  ggplot(aes(x=var1, y=var2)) +
  geom_point()

five_points <- five_points %>%  
  mutate(var1_odd_or_even = ifelse(var1%%2==0,"even","odd"),
         var2_odd_or_even = ifelse(var2%%2==0,"even","odd"))

five_points
five_points %>% 
  ggplot(aes(x=var1, y=var2,color = var1_odd_or_even)) +
  geom_point(size = 5) 

Aesthetics in ggplot

  • Color is for lines (including outlines of shapes)

  • Fill is for areas (filling in shapes)

    • but our points are filled in based on the color aesthetic?!

    • the default point shape is shape 19 (“circle”) - just a solid circle with the color taken from the color aesthetic; shape 21 (“filled circle”) is a circle with a fill and an outline, which can be specified separately

  • Generally, you can either map the aesthetic to a data point or specify its value outside of the aes() function. If you do neither, a default will be used.

  • Aesthetic mapping/specification can be done in the original ggplot() call, or within individual geoms

Good general reference on specifying aesthetics:

https://ggplot2.tidyverse.org/articles/ggplot2-specs.html#point-1

five_points %>% 
  ggplot(aes(x=var1, 
             y=var2,
             color = var1_odd_or_even,
             fill = var2_odd_or_even)) +
  geom_point(size = 5, 
             shape = 21,
             stroke = 2) 

Points as Maps

  • Longitude and Latitude are (on a 2d map) x and y coordinates

  • Let’s get some coordinate data from the NYC facilities database

https://data.cityofnewyork.us/City-Government/Facilities-Database/ji82-xba5

public_libraries <- jsonlite::fromJSON("https://data.cityofnewyork.us/resource/ji82-xba5.json?factype=PUBLIC%20LIBRARY") 

public_libraries %>% str()
'data.frame':   221 obs. of  34 variables:
 $ uid       : chr  "015fec883329f3481f1711a39386a5ee" "05375983dd54899915412bc855795f75" "0616b9a31fe2db14793561377be01c0e" "08d71649b166891363a946ebb0b3b029" ...
 $ facname   : chr  "MORRIS PARK LIBRARY" "NORTH FOREST PARK" "58TH STREET LIBRARY" "FLUSHING" ...
 $ addressnum: chr  "985" "98-27" "127" "41-17" ...
 $ streetname: chr  "MORRIS PARK AVENUE" "METROPOLITAN AVENUE" "EAST 58 STREET" "MAIN STREET" ...
 $ address   : chr  "985 MORRIS PARK AVENUE" "98-27 METROPOLITAN AVENUE" "127 EAST 58 STREET" "41-17 MAIN STREET" ...
 $ city      : chr  "BRONX" "FOREST HILLS" "NEW YORK" "FLUSHING" ...
 $ boro      : chr  "BRONX" "QUEENS" "MANHATTAN" "QUEENS" ...
 $ borocode  : chr  "2" "4" "1" "4" ...
 $ zipcode   : chr  "10462" "11375" "10022" "11355" ...
 $ latitude  : chr  "40.84806105240" "40.71118635740" "40.76223213860" "40.75778232920" ...
 $ longitude : chr  "-73.85697658820" "-73.85367190670" "-73.96932374730" "-73.82887651450" ...
 $ xcoord    : chr  "1023819.55661000000" "1024817.14797000000" "992747.99653700000" "1031658.10613000000" ...
 $ ycoord    : chr  "248281.25916200000" "198414.74758100000" "216979.94069500000" "215403.54794900000" ...
 $ bin       : chr  "2045398" "4076687" "1037165" "4114282" ...
 $ bbl       : chr  "2041260040.00000000000" "4032070022.00000000000" "1013130005.00000000000" "4050430011.00000000000" ...
 $ commboard : chr  "211" "406" "105" "407" ...
 $ council   : chr  "13" "29" "4" "20" ...
 $ censtract : chr  "25200" "72900" "11203" "85300" ...
 $ nta       : chr  "BX37" "QN17" "MN19" "QN22" ...
 $ facgroup  : chr  "LIBRARIES" "LIBRARIES" "LIBRARIES" "LIBRARIES" ...
 $ facsubgrp : chr  "PUBLIC LIBRARIES" "PUBLIC LIBRARIES" "PUBLIC LIBRARIES" "PUBLIC LIBRARIES" ...
 $ factype   : chr  "PUBLIC LIBRARY" "PUBLIC LIBRARY" "PUBLIC LIBRARY" "PUBLIC LIBRARY" ...
 $ capacity  : chr  "0" "0" "0" "0" ...
 $ optype    : chr  "Public" "Public" "Public" "Public" ...
 $ opname    : chr  "New York Public Library" "Queens Public Library" "New York Public Library" "Queens Public Library" ...
 $ opabbrev  : chr  "NYPL" "Public" "NYPL" "Public" ...
 $ overlevel : chr  "City" "City" "City" "City" ...
 $ overagency: chr  "New York Public Library" "Queens Public Library" "New York Public Library" "Queens Public Library" ...
 $ overabbrev: chr  "NYPL" "QPL" "NYPL" "QPL" ...
 $ datasource: chr  "nypl_libraries" "qpl_libraries" "nypl_libraries" "qpl_libraries" ...
 $ facdomain : chr  "LIBRARIES AND CULTURAL PROGRAMS" "LIBRARIES AND CULTURAL PROGRAMS" "LIBRARIES AND CULTURAL PROGRAMS" "LIBRARIES AND CULTURAL PROGRAMS" ...
 $ schooldist: chr  "11" "28" "2" "25" ...
 $ policeprct: chr  "49" "112" "18" "109" ...
 $ servarea  : chr  "Local" "Local" "Local" "Local" ...

We need latitude and longitude to be numeric, not strings

public_libraries <- public_libraries %>%  
  mutate(latitude=as.numeric(latitude),longitude=as.numeric(longitude))

public_libraries %>% 
  select(facname,latitude,longitude) %>% 
  str()
'data.frame':   221 obs. of  3 variables:
 $ facname  : chr  "MORRIS PARK LIBRARY" "NORTH FOREST PARK" "58TH STREET LIBRARY" "FLUSHING" ...
 $ latitude : num  40.8 40.7 40.8 40.8 40.7 ...
 $ longitude: num  -73.9 -73.9 -74 -73.8 -73.7 ...
  • Map the longitude and latitude to the x and y aesthetics

    • Everyone has a strategy for remembering which is which - I like to think of soup dumplings - “Xiao long bao” - credit to @seankross
public_libraries %>% 
  ggplot(aes(x=longitude,
             y=latitude)) + 
  geom_point() 

  • That does not look like a map. Let’s look at our data.
public_libraries %>% 
  select(latitude,longitude) %>% 
  summary()
    latitude       longitude     
 Min.   : 0.00   Min.   :-74.24  
 1st Qu.:40.66   1st Qu.:-73.98  
 Median :40.72   Median :-73.92  
 Mean   :40.35   Mean   :-73.25  
 3rd Qu.:40.77   3rd Qu.:-73.85  
 Max.   :40.90   Max.   :  0.00  
  • At least some of our lat/long values are way out of expected range
public_libraries %>% 
  filter(latitude<39|longitude>74) %>% 
  select(latitude,longitude,facname,addressnum,streetname,address,city,boro)
  • Looks like we can clean this up by just filtering out these two cases
 public_libraries <- public_libraries %>%   
  filter(!latitude==0,
         !longitude==0)
 
 public_libraries %>% nrow()
[1] 219
  • Try again
public_libraries %>% 
  ggplot(aes(x=longitude,
             y=latitude)) + 
  geom_point() 

  • Make it a bit prettier

    • Color the points and rename the legend with scale_color_manual()

    • Get rid of axes and background with theme_void()

#define some system colors - a named vector of colors will map the colors to the values of the color aesthetic (backticks let you use non-syntactic R names)
#color values can be names from R's built-in colors or hex codes

library_system_colors <- c(`New York Public Library`="red3",
                   `Brooklyn Public Library` = "orange2",
                   `Queens Public Library` = "purple3")

public_libraries %>% 
  ggplot(aes(x=longitude,
             y=latitude,
             color = overagency)) + 
  geom_point() +
  scale_color_manual(values = library_system_colors,name = "System") +
  theme_void()

The sf (simple features) package

https://r-spatial.github.io/sf/

library(sf)

Simple features is a set of standards for defining two-dimensional geometries, used by various GIS systems, building up from x-y coordinates within a coordinate reference system (crs).

sf package in R lets you:

You can make an sf object directly from a data frame:

five_points_sf <- five_points %>% st_as_sf(coords = c("var1","var2")) 

five_points_sf %>% print()
Simple feature collection with 5 features and 2 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 1 ymin: 1 xmax: 5 ymax: 5
CRS:           NA
  var1_odd_or_even var2_odd_or_even    geometry
1              odd              odd POINT (1 3)
2             even              odd POINT (2 5)
3              odd             even POINT (3 4)
4             even             even POINT (4 4)
5              odd              odd POINT (5 1)
five_points_sf %>% 
  ggplot() + 
  geom_sf(
    aes(color = var1_odd_or_even,
              fill = var2_odd_or_even),
          size = 5,
          shape = 21,
          stroke = 2
    )

Map Polygons

But for mapping we usually import geometry data from an external dataset.

Formats:

  • GEOJSON

  • ESRI shapefile

  • Many others (st_drivers(what = "vector"))

NYC geography resources:

https://www1.nyc.gov/site/planning/data-maps/open-data/census-download-metadata.page

  • Get the NYC PUMA (Public Use Microdata Area) geographies in GEOJSON format (PUMAs in NYC correspond - mostly - to Community Districts)
#read geojson format directly from download link
pumas_2010_geojson <- st_read("https://services5.arcgis.com/GfwWNkhOj9bNBqoJ/arcgis/rest/services/NYC_Public_Use_Microdata_Areas_PUMAs_2010/FeatureServer/0/query?where=1=1&outFields=*&outSR=4326&f=pgeojson") %>% 
  st_as_sf()
Reading layer `OGRGeoJSON' from data source 
  `https://services5.arcgis.com/GfwWNkhOj9bNBqoJ/arcgis/rest/services/NYC_Public_Use_Microdata_Areas_PUMAs_2010/FeatureServer/0/query?where=1=1&outFields=*&outSR=4326&f=pgeojson' 
  using driver `GeoJSON'
Simple feature collection with 55 features and 4 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -74.25559 ymin: 40.49612 xmax: -73.70001 ymax: 40.91554
Geodetic CRS:  WGS 84
pumas_2010_geojson %>% print()
Simple feature collection with 55 features and 4 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -74.25559 ymin: 40.49612 xmax: -73.70001 ymax: 40.91554
Geodetic CRS:  WGS 84
First 10 features:
   OBJECTID PUMA Shape__Area Shape__Length                       geometry
1         1 3701    97928415      53226.65 MULTIPOLYGON (((-73.89641 4...
2         2 3702   188993596     106167.48 MULTIPOLYGON (((-73.86477 4...
3         3 3703   267645154     305260.19 MULTIPOLYGON (((-73.78833 4...
4         4 3704   106217077      47970.15 MULTIPOLYGON (((-73.84793 4...
5         5 3705   122483734      68697.43 MULTIPOLYGON (((-73.87046 4...
6         6 3706    43887042      51825.93 MULTIPOLYGON (((-73.87773 4...
7         7 3707    42281072      37374.60 MULTIPOLYGON (((-73.89964 4...
8         8 3708    55881008      35002.58 MULTIPOLYGON (((-73.92478 4...
9         9 3709   124117798      73287.89 MULTIPOLYGON (((-73.83668 4...
10       10 3710   137760355      90067.74 MULTIPOLYGON (((-73.89681 4...
  • Shapefile format is also very common
#for shapefiles, download the zipped shapefile directory into your current working directory, then unzip (can do this outside R if you prefer!)
#download
download.file(url = "https://www1.nyc.gov/assets/planning/download/zip/data-maps/open-data/nypuma2010_21c.zip",destfile = "nypuma2010_21c.zip")
#unzip
unzip("nypuma2010_21c.zip")

#read in the shapefile
pumas_2010_shp <- st_read("nypuma2010_21c/nypuma2010.shp") %>% st_as_sf()
Reading layer `nypuma2010' from data source `/Users/sarahrankin/Documents/Mapping/nypuma2010_21c/nypuma2010.shp' using driver `ESRI Shapefile'
Simple feature collection with 55 features and 3 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: 913175.1 ymin: 120121.9 xmax: 1067383 ymax: 272844.3
Projected CRS: NAD83 / New York Long Island (ftUS)
pumas_2010_shp %>% print()
Simple feature collection with 55 features and 3 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: 913175.1 ymin: 120121.9 xmax: 1067383 ymax: 272844.3
Projected CRS: NAD83 / New York Long Island (ftUS)
First 10 features:
   PUMA Shape_Leng Shape_Area                       geometry
1  3701   53227.11   97928517 MULTIPOLYGON (((1012885 268...
2  3702  106167.59  188993632 MULTIPOLYGON (((1021632 267...
3  3703  305269.14  267643637 MULTIPOLYGON (((1042822 243...
4  3704   47970.20  106216909 MULTIPOLYGON (((1026309 256...
5  3705   68697.60  122483690 MULTIPOLYGON (((1020081 255...
6  3706   51826.07   43886931 MULTIPOLYGON (((1018060 261...
7  3707   37374.60   42281072 MULTIPOLYGON (((1012009 253...
8  3708   35002.71   55881068 MULTIPOLYGON (((1005061 247...
9  3709   73288.78  124117768 MULTIPOLYGON (((1029456 237...
10 3710   90067.96  137760290 MULTIPOLYGON (((1012822 229...
  • Again, adding geom_sf() to a ggplot renders the data in the geometry column - in this case it draws polygons because the geometry type is multipolygon
pumas_2010_geojson %>% 
  ggplot() + 
  geom_sf()

  • Add our points to this map by specifying different data sources within the geom_sf and geom_point calls

  • sf objects can have a CRS (coordinate reference system) defined

  ggplot() + 
  geom_sf(data = pumas_2010_geojson,
    fill = "grey",
    color = "grey"
    ) +
  geom_point(data = public_libraries, 
             aes(x=longitude,
                 y=latitude,
                 color = overagency)) +
  scale_color_manual(name = "System",values = library_system_colors) +
  #adding coord sf ensures crs matches
  coord_sf() +
  theme_void()

Chloropleths

  • Map areas colored by data value

Broadband adoption in New York City

broadband_use <- read_csv("https://data.cityofnewyork.us/api/views/g5ah-i2sh/rows.csv?accessType=DOWNLOAD")

── Column specification ────────────────────────────────────────────────────────────────────────────────────────────────
cols(
  `PUMA (Public Use Microdata Sample Areas)` = col_double(),
  Borough = col_character(),
  `Home Broadband and Mobile Broadband Adoption (Percentage of  Households)` = col_double(),
  `Home Broadband and Mobile Broadband Adoption by Quartiles (High, Medium-High, Medium-Low, Low)` = col_character()
)
#clean up the names a bit
broadband_use <- broadband_use %>% 
  rename(PUMA = `PUMA (Public Use Microdata Sample Areas)`,
         broadband_adoption = `Home Broadband and Mobile Broadband Adoption (Percentage of  Households)`,
         broadband_adoption_quartile = `Home Broadband and Mobile Broadband Adoption by Quartiles (High, Medium-High, Medium-Low, Low)`)

broadband_use %>% head()
  • Add the broadband data to our PUMA geom dataset
#convert PUMA to string; make adoption quartile into factor so it'll sort correctly  
broadband_use <- broadband_use %>% 
  mutate(PUMA = as.character(PUMA),
         broadband_adoption_quartile = factor(broadband_adoption_quartile,
                                              levels = c("High", "Medium High", "Medium Low", "Low")))

pumas_2010_geojson <- pumas_2010_geojson %>% 
  left_join(broadband_use, by = "PUMA")

pumas_2010_geojson %>% print()
Simple feature collection with 55 features and 7 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -74.25559 ymin: 40.49612 xmax: -73.70001 ymax: 40.91554
Geodetic CRS:  WGS 84
First 10 features:
   OBJECTID PUMA Shape__Area Shape__Length Borough broadband_adoption broadband_adoption_quartile
1         1 3701    97928415      53226.65   Bronx               0.46                        High
2         2 3702   188993596     106167.48   Bronx               0.30                         Low
3         3 3703   267645154     305260.19   Bronx               0.34                  Medium Low
4         4 3704   106217077      47970.15   Bronx               0.30                         Low
5         5 3705   122483734      68697.43   Bronx               0.32                         Low
6         6 3706    43887042      51825.93   Bronx               0.39                 Medium High
7         7 3707    42281072      37374.60   Bronx               0.42                        High
8         8 3708    55881008      35002.58   Bronx               0.40                 Medium High
9         9 3709   124117798      73287.89   Bronx               0.29                         Low
10       10 3710   137760355      90067.74   Bronx               0.34                  Medium Low
                         geometry
1  MULTIPOLYGON (((-73.89641 4...
2  MULTIPOLYGON (((-73.86477 4...
3  MULTIPOLYGON (((-73.78833 4...
4  MULTIPOLYGON (((-73.84793 4...
5  MULTIPOLYGON (((-73.87046 4...
6  MULTIPOLYGON (((-73.87773 4...
7  MULTIPOLYGON (((-73.89964 4...
8  MULTIPOLYGON (((-73.92478 4...
9  MULTIPOLYGON (((-73.83668 4...
10 MULTIPOLYGON (((-73.89681 4...
  • Map the geom_sf fill aesthetic to broadband_adoption

  • Use scale_fill_gradient to specify fill colors

  ggplot() + 
  geom_sf(data = pumas_2010_geojson,
    aes(fill = broadband_adoption),
    color = "grey"
    ) +
  geom_point(data = public_libraries,
             aes(x=longitude,
                 y=latitude,
                 color = overagency),
             size = 2) +
  scale_color_manual(name = "System",values = library_system_colors) +
  scale_fill_gradient(low = "grey70",high = "grey10", labels = scales::percent_format(accuracy = 1), name = "Broadband adoption") +
  coord_sf() +
  theme_void()

  • Or define discrete colors and use scale_fill_manual
broadband_cols <- c("grey10","grey30","grey50","grey70") %>% set_names(levels(pumas_2010_geojson$broadband_adoption_quartile))


ggplot() + 
  geom_sf(data = pumas_2010_geojson,
    aes(fill = broadband_adoption_quartile),
    color = "grey"
    ) +
  geom_point(data = public_libraries,
             aes(x=longitude,
                 y=latitude,
                 color = overagency),
             size = 2) +
  scale_color_manual(name = "System",values = library_system_colors) +
  scale_fill_manual(values = broadband_cols, name = "Broadband adoption") +
  coord_sf() +
  theme_void()

Median income

https://www1.nyc.gov/site/planning/planning-level/nyc-population/american-community-survey.page

  • Download data and add it to our pumas_2010_geojson data frame
download.file(url = "https://www1.nyc.gov/assets/planning/download/office/planning-level/nyc-population/acs/econ_2018_acs5yr_puma.xlsx",
              destfile = "econ_2018_acs5yr_puma.xlsx")

econdata_puma <- readxl::read_xlsx("econ_2018_acs5yr_puma.xlsx", sheet = "EconData")

pumas_2010_geojson <- pumas_2010_geojson %>% 
  left_join(econdata_puma %>% 
              select(PUMA = GeoID,GeogName,median_household_income = MdHHIncE) %>% 
              mutate(median_income_category = 
                       cut(median_household_income,
                           breaks = c(0,50000,75000,100000,150000),
                           labels = c("$0-50K","$50-75K","$75-100K","$100K+"))))
Joining, by = "PUMA"
  • Create a new color scale with some greens
median_income_category_cols <- c("#a9ccbc","#7fb29b","#4c7f68","#335545") %>% set_names(levels(pumas_2010_geojson$median_income_category))

ggplot() + 
  geom_sf(data = pumas_2010_geojson,
    aes(fill = median_income_category),
    color = "grey"
    ) +
  geom_point(data = public_libraries,
             aes(x=longitude,
                 y=latitude,
                 color = overagency),
             size = 2) +
  scale_color_manual(name = "System",values = library_system_colors) +
  scale_fill_manual(values = median_income_category_cols, name = "Median income") +
  coord_sf() +
  theme_void() 

Leaflet: interactive maps

library(leaflet)

Leaflet: open source javascript library for making interactive maps on various platforms

R has a leaflet package that lets you create leaflet “widgets” within R

Create a map, add tiles, set the view

leaflet() %>% 
  #addTiles() %>% 
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lat = 40.7,lng = -74, zoom = 10)

Add our public library branches

leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lat = 40.7,lng = -74, zoom = 10) %>%
  addCircles(data = public_libraries,
             lng = ~longitude,
             lat = ~latitude
             )
pal_system <- colorFactor(palette = library_system_colors ,
                          domain = unique(public_libraries$overagency),
                          ordered = T)

leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lat = 40.7,lng = -74, zoom = 10) %>%
  addCircles(data = public_libraries,
             lng = ~longitude,
             lat = ~latitude,
             color = ~pal_system(overagency),
             fill = ~pal_system(overagency),
             radius = 100,
             label = ~facname,
             popup = ~address
             ) %>% 
  addLegend(position = "topleft",
            pal = pal_system, 
            values = unique(public_libraries$overagency))

Because we have a basemap, we don’t need the PUMA polygons to create a basic map. But we can add them if we want our chloropleths on the interactive map.

pal_broadband <- colorFactor(palette = broadband_cols, 
                             domain = levels(pumas_2010_geojson$broadband_adoption_quartile),
                             ordered = T)


leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lat = 40.7,lng = -74, zoom = 10) %>%
  addPolygons(data = pumas_2010_geojson,
              fillColor = ~pal_broadband(broadband_adoption_quartile),
              fillOpacity = .7,
              color = "grey",
              stroke = T,
              weight = 1,
              label = ~PUMA,
              popup = ~paste0("<b>PUMA ",PUMA,"</b>",
                             "<br>", GeogName,
                             "<br>Broadband adoption: ",round(broadband_adoption*100,0),"%",
                             "<br>Broadband adoption category: ",broadband_adoption_quartile)) 

Combine it with the libraries and add a legend

leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lat = 40.7,lng = -74, zoom = 10) %>%
  addPolygons(data = pumas_2010_geojson,
              fillColor = ~pal_broadband(broadband_adoption_quartile),
              fillOpacity = .7,
              color = "grey",
              stroke = T,
              weight = 1,
              label = ~PUMA,
              popup = ~paste0("<b>PUMA ",PUMA,"</b>",
                              "<br>", GeogName,
                             "<br>Broadband adoption: ",round(broadband_adoption*100,0),"%",
                             "<br>Broadband adoption category: ",broadband_adoption_quartile)) %>% 
    addCircles(data = public_libraries,
             lng = ~longitude,
             lat = ~latitude,
             color = ~pal_system(overagency),
             fill = ~pal_system(overagency),
             radius = 100,
             label = ~facname,
             popup = ~address
             ) %>% 
  addLegend(position = "topleft",
            pal = pal_system, 
            values = public_libraries$overagency,
            title = "Library System") %>% 
  addLegend(position = "topleft",
            pal = pal_broadband, 
            values =  pumas_2010_geojson$broadband_adoption_quartile,
            opacity = .7,
            title = "Broadband Access")

Visualize more than one layer

pal_income <- colorFactor(palette = median_income_category_cols, 
                             domain = levels(pumas_2010_geojson$median_income_category),
                             ordered = T)

libraries_map <- leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lat = 40.7,lng = -74, zoom = 10) %>%
  addPolygons(data = pumas_2010_geojson,
              fillColor = ~pal_broadband(broadband_adoption_quartile),
              fillOpacity = .7,
              color = "grey",
              stroke = T,
              weight = 1,
              label = ~PUMA,
              popup = ~paste0("<b>PUMA ",PUMA,"</b>",
                              "<br>", GeogName,
                             "<br>Broadband adoption: ",round(broadband_adoption*100,0),"%",
                             "<br>Broadband adoption category: ",broadband_adoption_quartile),
              group = "Broadband") %>% 
  addPolygons(data = pumas_2010_geojson,
              fillColor = ~pal_income(median_income_category),
              fillOpacity = .7,
              color = "grey",
              stroke = T,
              weight = 1,
              label = ~PUMA,
              popup = ~paste0("<b>PUMA ",PUMA,"</b>",
                              "<br>", GeogName,
                             "<br>Median income: $",format(median_household_income,big.mark = ","),
                             "<br>Median income category: ",median_income_category),
              group = "income") %>% 
    addCircles(data = public_libraries,
             lng = ~longitude,
             lat = ~latitude,
             color = ~pal_system(overagency),
             fill = ~pal_system(overagency),
             opacity = 1,
             fillOpacity = 1,
             radius = 100,
             label = ~facname,
             popup = ~address#,
             #group = "Broadband"
             ) %>% 
    # addCircles(data = public_libraries,
    #          lng = ~longitude,
    #          lat = ~latitude,
    #          color = ~pal_system(overagency),
    #          fill = ~pal_system(overagency),
    #          opacity = 1,
    #          fillOpacity = 1,
    #          radius = 100,
    #          label = ~facname,
    #          popup = ~address,
    #          group = "Income"
    #          ) %>% 
  addLegend(position = "topleft",
            pal = pal_system, 
            values = public_libraries$overagency,
            title = "Library System") %>% 
  addLegend(position = "topleft",
            pal = pal_broadband, 
            values =  pumas_2010_geojson$broadband_adoption_quartile,
            opacity = .7,
            title = "Broadband Access",
            group = "Broadband") %>% 
  addLegend(position = "topleft",
            pal = pal_income, 
            values =  pumas_2010_geojson$median_income_category,
            opacity = .7,
            title = "Median household income",
            group = "Income") %>% 
  addLayersControl(
            baseGroups = c("Broadband","Income"),
            options = layersControlOptions(collapsed = F)#,
            #overlayGroups = c("Branches")
  ) 


libraries_map

The htmlwidgets package lets you save this map as an html file, which can then be opened in a browser or embedded in a web page

htmlwidgets::saveWidget(libraries_map,
                        file = "libraries_map.html",
                        title = "NYC Libraries:  Broadband Access and Median Income")

Other resources

LS0tCnRpdGxlOiAiTWFraW5nIE1hcHMgaW4gUiIKb3V0cHV0OgogIGh0bWxfZG9jdW1lbnQ6CiAgICBkZl9wcmludDogcGFnZWQKZWRpdG9yX29wdGlvbnM6CiAgY2h1bmtfb3V0cHV0X3R5cGU6IGlubGluZQotLS0KCnwgKkFuRXggTGVhcm5pbmcgU3VtbWl0Kgp8ICpUaHVyc2RheSBPY3RvYmVyIDIxLCAyMDIxKgp8IAp8ICpTYXJhaCBSYW5raW4qCnwgKlF1YW50aXRhdGl2ZSBEYXRhIEFuYWx5c3QqCnwgKlN0YXRlZ3kgYW5kIFB1YmxpYyBJbXBhY3QqCnwgKk5ldyBZb3JrIFB1YmxpYyBMaWJyYXJ5Kgp8ICpzYXJhaHJhbmtpblxAbnlwbC5vcmcqCnwgCgojIyBUb3BpY3MKCi0gICBTdGF0aWMgbWFwcyB3aXRoIHNmIGFuZCBnZ3Bsb3QKLSAgIER5bmFtaWMgbWFwcyB3aXRoIGxlYWZsZXQKCiMjIFNldHVwIAoKVGhpcyBpcyBhbiBSbWFya2Rvd24gbm90ZWJvb2suIElmIHlvdSdyZSB2aWV3aW5nIGl0IGluIGEgd2ViIGJyb3dzZXIsIHlvdSBjYW4gY2xpY2sgb24gQ29kZS1cPkRvd25sb2FkIFJtZCBpbiB0aGUgdG9wIHJpZ2h0IGNvcm5lciwgdGhlbiBvcGVuIGl0IGluIFJTdHVkaW8gYW5kIHRvIHJ1bi9lZGl0IHRoZSBjb2RlLiBJZiB5b3UgZG9uJ3QgdXNlIFJTdHVkaW8geW91IGNhbiBhbHNvIGp1c3QgY29weSB0aGUgY29kZSBpbnRvIHlvdXIgZWRpdG9yIG9mIGNob2ljZS4KCldlJ2xsIGJlIHVzaW5nIGEgdmFyaWV0eSBvZiBgdGlkeXZlcnNlYCBwYWNrYWdlcyBhcyB3ZWxsIGFzIGBzZmAgYW5kIGBsZWFmbGV0YC4gSWYgeW91IGRvbid0IGhhdmUgdGhlc2UgcGFja2FnZXMgaW5zdGFsbGVkLCB1bmNvbW1lbnQgdGhlIGZvbGxvd2luZyBjb2RlIGFuZCBpbnN0YWxsIHRoZW0gbm93LiAoSXQnbGwgdGFrZSBhIGxpdHRsZSB3aGlsZS4pCgpgYGB7cn0KIyBpbnN0YWxsLnBhY2thZ2VzKCJ0aWR5dmVyc2UiKQojIGluc3RhbGwucGFja2FnZXMoInNmIikKIyBpbnN0YWxsLnBhY2thZ2VzKCJsZWFmbGV0IikKCmBgYAoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KI2xvYWQganVzdCB0aGUgdGlkeXZlcnNlIGZvciBub3cKbGlicmFyeSh0aWR5dmVyc2UpCgpgYGAKCiMjIFF1aWNrIGdncGxvdCByZWZyZXNoZXIKCkxldCdzIGNyZWF0ZSBzb21lIHZlcnkgc2ltcGxlIHBvaW50IGRhdGEuCgpgYGB7cn0KZml2ZV9wb2ludHMgPC0gZGF0YS5mcmFtZSh2YXIxID0gMTo1LCB2YXIyID0gYXMuaW50ZWdlcihjKDMsNSw0LDQsMSkpKQpmaXZlX3BvaW50cyAKYGBgCgotICAgQ2FsbCBgZ2dwbG90KClgIHRvIG1ha2UgYSBjYW52YXMKCi0gICBEZWZpbmUgYWVzdGhldGljcyB3aXRoIGBhZXMoKWAgLSB0aGVzZSBtYXAgeW91ciBkYXRhIHRvIHNwYXRpYWwgcHJvcGVydGllcyBsaWtlIHRoZSB4IGFuZCB5IGRpbWVuc2lvbnMsIG9yIHZpc3VhbCBhZXN0aGV0aWNzIGxpa2UgY29sb3IsIGZpbGwsIGFscGhhIGV0YwoKLSAgIEFkZCBhIGdlb20gLSBoZXJlLCBgZ2VvbV9wb2ludCgpYAoKYGBge3J9CmZpdmVfcG9pbnRzICU+JSAKICBnZ3Bsb3QoYWVzKHg9dmFyMSwgeT12YXIyKSkgKwogIGdlb21fcG9pbnQoKQogIApgYGAKCi0gICBBZGQgZmxhZ3MgaW5kaWNhdGluZyB3aGV0aGVyIGVhY2ggdmFyaWFibGUgaXMgZXZlbiBvciBvZGQgKGAlJWAgaXMgdGhlIG1vZHVsbyBmdW5jdGlvbikKCmBgYHtyfQpmaXZlX3BvaW50cyA8LSBmaXZlX3BvaW50cyAlPiUgIAogIG11dGF0ZSh2YXIxX29kZF9vcl9ldmVuID0gaWZlbHNlKHZhcjElJTI9PTAsImV2ZW4iLCJvZGQiKSwKICAgICAgICAgdmFyMl9vZGRfb3JfZXZlbiA9IGlmZWxzZSh2YXIyJSUyPT0wLCJldmVuIiwib2RkIikpCgpmaXZlX3BvaW50cwoKCmBgYAoKLSAgIE1hcCAidmFyXzFcX2lzIGV2ZW4iIHRvIHRoZSBjb2xvciBhZXN0aGV0aWMKCmBgYHtyfQpmaXZlX3BvaW50cyAlPiUgCiAgZ2dwbG90KGFlcyh4PXZhcjEsIHk9dmFyMixjb2xvciA9IHZhcjFfb2RkX29yX2V2ZW4pKSArCiAgZ2VvbV9wb2ludChzaXplID0gNSkgCgoKYGBgCgojIyMjIEFlc3RoZXRpY3MgaW4gZ2dwbG90CgotICAgQ29sb3IgaXMgZm9yIGxpbmVzIChpbmNsdWRpbmcgb3V0bGluZXMgb2Ygc2hhcGVzKQoKLSAgIEZpbGwgaXMgZm9yIGFyZWFzIChmaWxsaW5nIGluIHNoYXBlcykKCiAgICAtICAgYnV0IG91ciBwb2ludHMgYXJlIGZpbGxlZCBpbiBiYXNlZCBvbiB0aGUgY29sb3IgYWVzdGhldGljPyEKCiAgICAtICAgdGhlIGRlZmF1bHQgcG9pbnQgc2hhcGUgaXMgc2hhcGUgMTkgKCJjaXJjbGUiKSAtIGp1c3QgYSBzb2xpZCBjaXJjbGUgd2l0aCB0aGUgY29sb3IgdGFrZW4gZnJvbSB0aGUgY29sb3IgYWVzdGhldGljOyBzaGFwZSAyMSAoImZpbGxlZCBjaXJjbGUiKSBpcyBhIGNpcmNsZSB3aXRoIGEgZmlsbCBhbmQgYW4gb3V0bGluZSwgd2hpY2ggY2FuIGJlIHNwZWNpZmllZCBzZXBhcmF0ZWx5CgotICAgR2VuZXJhbGx5LCB5b3UgY2FuIGVpdGhlciBtYXAgdGhlIGFlc3RoZXRpYyB0byBhIGRhdGEgcG9pbnQgb3Igc3BlY2lmeSBpdHMgdmFsdWUgb3V0c2lkZSBvZiB0aGUgYGFlcygpYCBmdW5jdGlvbi4gSWYgeW91IGRvIG5laXRoZXIsIGEgZGVmYXVsdCB3aWxsIGJlIHVzZWQuCgotICAgQWVzdGhldGljIG1hcHBpbmcvc3BlY2lmaWNhdGlvbiBjYW4gYmUgZG9uZSBpbiB0aGUgb3JpZ2luYWwgZ2dwbG90KCkgY2FsbCwgb3Igd2l0aGluIGluZGl2aWR1YWwgZ2VvbXMKCkdvb2QgZ2VuZXJhbCByZWZlcmVuY2Ugb24gc3BlY2lmeWluZyBhZXN0aGV0aWNzOgoKPGh0dHBzOi8vZ2dwbG90Mi50aWR5dmVyc2Uub3JnL2FydGljbGVzL2dncGxvdDItc3BlY3MuaHRtbCNwb2ludC0xPgoKYGBge3J9CmZpdmVfcG9pbnRzICU+JSAKICBnZ3Bsb3QoYWVzKHg9dmFyMSwgCiAgICAgICAgICAgICB5PXZhcjIsCiAgICAgICAgICAgICBjb2xvciA9IHZhcjFfb2RkX29yX2V2ZW4sCiAgICAgICAgICAgICBmaWxsID0gdmFyMl9vZGRfb3JfZXZlbikpICsKICBnZW9tX3BvaW50KHNpemUgPSA1LCAKICAgICAgICAgICAgIHNoYXBlID0gMjEsCiAgICAgICAgICAgICBzdHJva2UgPSAyKSAKCmBgYAoKIyMjIFBvaW50cyBhcyBNYXBzCgotICAgTG9uZ2l0dWRlIGFuZCBMYXRpdHVkZSBhcmUgKG9uIGEgMmQgbWFwKSB4IGFuZCB5IGNvb3JkaW5hdGVzCgotICAgTGV0J3MgZ2V0IHNvbWUgY29vcmRpbmF0ZSBkYXRhIGZyb20gdGhlIE5ZQyBmYWNpbGl0aWVzIGRhdGFiYXNlCgo8aHR0cHM6Ly9kYXRhLmNpdHlvZm5ld3lvcmsudXMvQ2l0eS1Hb3Zlcm5tZW50L0ZhY2lsaXRpZXMtRGF0YWJhc2Uvamk4Mi14YmE1PgoKYGBge3J9CnB1YmxpY19saWJyYXJpZXMgPC0ganNvbmxpdGU6OmZyb21KU09OKCJodHRwczovL2RhdGEuY2l0eW9mbmV3eW9yay51cy9yZXNvdXJjZS9qaTgyLXhiYTUuanNvbj9mYWN0eXBlPVBVQkxJQyUyMExJQlJBUlkiKSAKCnB1YmxpY19saWJyYXJpZXMgJT4lIHN0cigpCmBgYAoKV2UgbmVlZCBsYXRpdHVkZSBhbmQgbG9uZ2l0dWRlIHRvIGJlIG51bWVyaWMsIG5vdCBzdHJpbmdzCgpgYGB7cn0KcHVibGljX2xpYnJhcmllcyA8LSBwdWJsaWNfbGlicmFyaWVzICU+JSAgCiAgbXV0YXRlKGxhdGl0dWRlPWFzLm51bWVyaWMobGF0aXR1ZGUpLGxvbmdpdHVkZT1hcy5udW1lcmljKGxvbmdpdHVkZSkpCgpwdWJsaWNfbGlicmFyaWVzICU+JSAKICBzZWxlY3QoZmFjbmFtZSxsYXRpdHVkZSxsb25naXR1ZGUpICU+JSAKICBzdHIoKQoKCmBgYAoKLSAgIE1hcCB0aGUgbG9uZ2l0dWRlIGFuZCBsYXRpdHVkZSB0byB0aGUgeCBhbmQgeSBhZXN0aGV0aWNzCgogICAgLSAgIEV2ZXJ5b25lIGhhcyBhIHN0cmF0ZWd5IGZvciByZW1lbWJlcmluZyB3aGljaCBpcyB3aGljaCAtIEkgbGlrZSB0byB0aGluayBvZiBzb3VwIGR1bXBsaW5ncyAtICIqKlgqKmlhbyAqKmxvbmcqKiBiYW8iIC0gY3JlZGl0IHRvIFtcQHNlYW5rcm9zc10oaHR0cHM6Ly90d2l0dGVyLmNvbS9zZWFua3Jvc3Mvc3RhdHVzLzExMzQzMjY3Mjg0NzY3MTI5NjApCgpgYGB7cn0KCnB1YmxpY19saWJyYXJpZXMgJT4lIAogIGdncGxvdChhZXMoeD1sb25naXR1ZGUsCiAgICAgICAgICAgICB5PWxhdGl0dWRlKSkgKyAKICBnZW9tX3BvaW50KCkgCgoKCmBgYAoKLSAgIFRoYXQgZG9lcyBub3QgbG9vayBsaWtlIGEgbWFwLiBMZXQncyBsb29rIGF0IG91ciBkYXRhLgoKYGBge3J9CnB1YmxpY19saWJyYXJpZXMgJT4lIAogIHNlbGVjdChsYXRpdHVkZSxsb25naXR1ZGUpICU+JSAKICBzdW1tYXJ5KCkKCgpgYGAKCi0gICBBdCBsZWFzdCBzb21lIG9mIG91ciBsYXQvbG9uZyB2YWx1ZXMgYXJlIHdheSBvdXQgb2YgZXhwZWN0ZWQgcmFuZ2UKCmBgYHtyfQpwdWJsaWNfbGlicmFyaWVzICU+JSAKICBmaWx0ZXIobGF0aXR1ZGU8Mzl8bG9uZ2l0dWRlPjc0KSAlPiUgCiAgc2VsZWN0KGxhdGl0dWRlLGxvbmdpdHVkZSxmYWNuYW1lLGFkZHJlc3NudW0sc3RyZWV0bmFtZSxhZGRyZXNzLGNpdHksYm9ybykKCgpgYGAKCi0gICBMb29rcyBsaWtlIHdlIGNhbiBjbGVhbiB0aGlzIHVwIGJ5IGp1c3QgZmlsdGVyaW5nIG91dCB0aGVzZSB0d28gY2FzZXMKCmBgYHtyfQoKIHB1YmxpY19saWJyYXJpZXMgPC0gcHVibGljX2xpYnJhcmllcyAlPiUgICAKICBmaWx0ZXIoIWxhdGl0dWRlPT0wLAogICAgICAgICAhbG9uZ2l0dWRlPT0wKQogCiBwdWJsaWNfbGlicmFyaWVzICU+JSBucm93KCkKCmBgYAoKLSAgIFRyeSBhZ2FpbgoKYGBge3J9CnB1YmxpY19saWJyYXJpZXMgJT4lIAogIGdncGxvdChhZXMoeD1sb25naXR1ZGUsCiAgICAgICAgICAgICB5PWxhdGl0dWRlKSkgKyAKICBnZW9tX3BvaW50KCkgCgpgYGAKCi0gICBNYWtlIGl0IGEgYml0IHByZXR0aWVyCgogICAgLSAgIENvbG9yIHRoZSBwb2ludHMgYW5kIHJlbmFtZSB0aGUgbGVnZW5kIHdpdGggYHNjYWxlX2NvbG9yX21hbnVhbCgpYAoKICAgIC0gICBHZXQgcmlkIG9mIGF4ZXMgYW5kIGJhY2tncm91bmQgd2l0aCBgdGhlbWVfdm9pZCgpYAoKYGBge3J9CiNkZWZpbmUgc29tZSBzeXN0ZW0gY29sb3JzIC0gYSBuYW1lZCB2ZWN0b3Igb2YgY29sb3JzIHdpbGwgbWFwIHRoZSBjb2xvcnMgdG8gdGhlIHZhbHVlcyBvZiB0aGUgY29sb3IgYWVzdGhldGljIChiYWNrdGlja3MgbGV0IHlvdSB1c2Ugbm9uLXN5bnRhY3RpYyBSIG5hbWVzKQojY29sb3IgdmFsdWVzIGNhbiBiZSBuYW1lcyBmcm9tIFIncyBidWlsdC1pbiBjb2xvcnMgb3IgaGV4IGNvZGVzCgpsaWJyYXJ5X3N5c3RlbV9jb2xvcnMgPC0gYyhgTmV3IFlvcmsgUHVibGljIExpYnJhcnlgPSJyZWQzIiwKICAgICAgICAgICAgICAgICAgIGBCcm9va2x5biBQdWJsaWMgTGlicmFyeWAgPSAib3JhbmdlMiIsCiAgICAgICAgICAgICAgICAgICBgUXVlZW5zIFB1YmxpYyBMaWJyYXJ5YCA9ICJwdXJwbGUzIikKCnB1YmxpY19saWJyYXJpZXMgJT4lIAogIGdncGxvdChhZXMoeD1sb25naXR1ZGUsCiAgICAgICAgICAgICB5PWxhdGl0dWRlLAogICAgICAgICAgICAgY29sb3IgPSBvdmVyYWdlbmN5KSkgKyAKICBnZW9tX3BvaW50KCkgKwogIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXMgPSBsaWJyYXJ5X3N5c3RlbV9jb2xvcnMsbmFtZSA9ICJTeXN0ZW0iKSArCiAgdGhlbWVfdm9pZCgpCgoKCmBgYAoKIyMgVGhlIGBzZmAgKHNpbXBsZSBmZWF0dXJlcykgcGFja2FnZQoKPGh0dHBzOi8vci1zcGF0aWFsLmdpdGh1Yi5pby9zZi8+CgpgYGB7ciB9CmxpYnJhcnkoc2YpCmBgYAoKU2ltcGxlIGZlYXR1cmVzIGlzIGEgc2V0IG9mIHN0YW5kYXJkcyBmb3IgZGVmaW5pbmcgdHdvLWRpbWVuc2lvbmFsIGdlb21ldHJpZXMsIHVzZWQgYnkgdmFyaW91cyBHSVMgc3lzdGVtcywgYnVpbGRpbmcgdXAgZnJvbSB4LXkgY29vcmRpbmF0ZXMgd2l0aGluIGEgY29vcmRpbmF0ZSByZWZlcmVuY2Ugc3lzdGVtIChjcnMpLgoKLSAgIFBPSU5UCgotICAgTElORVNUUklORwoKLSAgIFBPTFlHT04KCi0gICBNVUxUSVBPSU5UCgotICAgTVVMVElQT0xZR09OCgpgc2ZgIHBhY2thZ2UgaW4gUiBsZXRzIHlvdToKCi0gICBzdG9yZSBnZW9tZXRyeSBkYXRhIGFzIGEgbGlzdC1jb2x1bW4gaW4gYSBkYXRhIGZyYW1lIG9yIHRpYmJsZSwgYWxvbmdzaWRlIG90aGVyIGRhdGEgY29sdW1ucwoKLSAgIGltcG9ydCBkYXRhIGludG8gdGhlIFIgc2YgZm9ybWF0IGZyb20gdmFyaW91cyBHSVMgZm9ybWF0cwoKLSAgIG1hbmlwdWxhdGUgc2YgZ2VvbWV0cmllcyAoY29uY2F0ZW5hdGUsIHN1YnRyYWN0LCBmaW5kIG92ZXJsYXBzLCBldGMpCgpZb3UgY2FuIG1ha2UgYW4gYHNmYCBvYmplY3QgZGlyZWN0bHkgZnJvbSBhIGRhdGEgZnJhbWU6CgpgYGB7cn0KCmZpdmVfcG9pbnRzX3NmIDwtIGZpdmVfcG9pbnRzICU+JSBzdF9hc19zZihjb29yZHMgPSBjKCJ2YXIxIiwidmFyMiIpKSAKCmZpdmVfcG9pbnRzX3NmICU+JSBwcmludCgpCmBgYAoKLSAgIFRoZXJlIGlzIGEgc3BlY2lhbCBnZW9tIGZvciBzZiBvYmplY3RzOiBgZ2VvbV9zZmAKCi0gICBXaGVuIHlvdSBhZGQgZ2VvbV9zZiB0byBhIHBsb3QgKGFuZCB0aGUgYHNmYCBwYWNrYWdlIGlzIGxvYWRlZCksIGdncGxvdCB3aWxsIHBsb3QgdGhlIGdlb21ldHJ5IGNvbHVtbiAtIHBsb3R0aW5nIG1ldGhvZCBkZXBlbmRzIG9uIHRoZSBzZiBnZW9tZXRyeSB0eXBlIChwb2ludCwgcG9seWdvbiwgZXRjKQoKYGBge3J9CgpmaXZlX3BvaW50c19zZiAlPiUgCiAgZ2dwbG90KCkgKyAKICBnZW9tX3NmKAogICAgYWVzKGNvbG9yID0gdmFyMV9vZGRfb3JfZXZlbiwKICAgICAgICAgICAgICBmaWxsID0gdmFyMl9vZGRfb3JfZXZlbiksCiAgICAgICAgICBzaXplID0gNSwKICAgICAgICAgIHNoYXBlID0gMjEsCiAgICAgICAgICBzdHJva2UgPSAyCiAgICApCgoKYGBgCgojIyMgTWFwIFBvbHlnb25zCgpCdXQgZm9yIG1hcHBpbmcgd2UgdXN1YWxseSBpbXBvcnQgZ2VvbWV0cnkgZGF0YSBmcm9tIGFuIGV4dGVybmFsIGRhdGFzZXQuCgpGb3JtYXRzOgoKLSAgIEdFT0pTT04KCi0gICBFU1JJIHNoYXBlZmlsZQoKLSAgIE1hbnkgb3RoZXJzIChgc3RfZHJpdmVycyh3aGF0ID0gInZlY3RvciIpYCkKCk5ZQyBnZW9ncmFwaHkgcmVzb3VyY2VzOgoKPGh0dHBzOi8vd3d3MS5ueWMuZ292L3NpdGUvcGxhbm5pbmcvZGF0YS1tYXBzL29wZW4tZGF0YS9jZW5zdXMtZG93bmxvYWQtbWV0YWRhdGEucGFnZT4KCi0gICBHZXQgdGhlIE5ZQyBQVU1BIChQdWJsaWMgVXNlIE1pY3JvZGF0YSBBcmVhKSBnZW9ncmFwaGllcyBpbiBHRU9KU09OIGZvcm1hdCAoUFVNQXMgaW4gTllDIGNvcnJlc3BvbmQgLSBtb3N0bHkgLSB0byBDb21tdW5pdHkgRGlzdHJpY3RzKQoKYGBge3IgY2FjaGU9VFJVRX0KCiNyZWFkIGdlb2pzb24gZm9ybWF0IGRpcmVjdGx5IGZyb20gZG93bmxvYWQgbGluawpwdW1hc18yMDEwX2dlb2pzb24gPC0gc3RfcmVhZCgiaHR0cHM6Ly9zZXJ2aWNlczUuYXJjZ2lzLmNvbS9HZndXTmtoT2o5Yk5CcW9KL2FyY2dpcy9yZXN0L3NlcnZpY2VzL05ZQ19QdWJsaWNfVXNlX01pY3JvZGF0YV9BcmVhc19QVU1Bc18yMDEwL0ZlYXR1cmVTZXJ2ZXIvMC9xdWVyeT93aGVyZT0xPTEmb3V0RmllbGRzPSomb3V0U1I9NDMyNiZmPXBnZW9qc29uIikgJT4lIAogIHN0X2FzX3NmKCkKCnB1bWFzXzIwMTBfZ2VvanNvbiAlPiUgcHJpbnQoKQoKIAoKCmBgYAoKLSAgIFNoYXBlZmlsZSBmb3JtYXQgaXMgYWxzbyB2ZXJ5IGNvbW1vbgoKYGBge3IgY2FjaGU9VFJVRX0KI2ZvciBzaGFwZWZpbGVzLCBkb3dubG9hZCB0aGUgemlwcGVkIHNoYXBlZmlsZSBkaXJlY3RvcnkgaW50byB5b3VyIGN1cnJlbnQgd29ya2luZyBkaXJlY3RvcnksIHRoZW4gdW56aXAgKGNhbiBkbyB0aGlzIG91dHNpZGUgUiBpZiB5b3UgcHJlZmVyISkKI2Rvd25sb2FkCmRvd25sb2FkLmZpbGUodXJsID0gImh0dHBzOi8vd3d3MS5ueWMuZ292L2Fzc2V0cy9wbGFubmluZy9kb3dubG9hZC96aXAvZGF0YS1tYXBzL29wZW4tZGF0YS9ueXB1bWEyMDEwXzIxYy56aXAiLGRlc3RmaWxlID0gIm55cHVtYTIwMTBfMjFjLnppcCIpCiN1bnppcAp1bnppcCgibnlwdW1hMjAxMF8yMWMuemlwIikKCiNyZWFkIGluIHRoZSBzaGFwZWZpbGUKcHVtYXNfMjAxMF9zaHAgPC0gc3RfcmVhZCgibnlwdW1hMjAxMF8yMWMvbnlwdW1hMjAxMC5zaHAiKSAlPiUgc3RfYXNfc2YoKQoKCnB1bWFzXzIwMTBfc2hwICU+JSBwcmludCgpCgpgYGAKCi0gICBBZ2FpbiwgYWRkaW5nIGBnZW9tX3NmKClgIHRvIGEgZ2dwbG90IHJlbmRlcnMgdGhlIGRhdGEgaW4gdGhlIGdlb21ldHJ5IGNvbHVtbiAtIGluIHRoaXMgY2FzZSBpdCBkcmF3cyBwb2x5Z29ucyBiZWNhdXNlIHRoZSBnZW9tZXRyeSB0eXBlIGlzIG11bHRpcG9seWdvbgoKYGBge3IgfQpwdW1hc18yMDEwX2dlb2pzb24gJT4lIAogIGdncGxvdCgpICsgCiAgZ2VvbV9zZigpCgpgYGAKCi0gICBBZGQgb3VyIHBvaW50cyB0byB0aGlzIG1hcCBieSBzcGVjaWZ5aW5nIGRpZmZlcmVudCBkYXRhIHNvdXJjZXMgd2l0aGluIHRoZSBgZ2VvbV9zZmAgYW5kIGBnZW9tX3BvaW50YCBjYWxscwoKLSAgIGBzZmAgb2JqZWN0cyBjYW4gaGF2ZSBhIENSUyAoY29vcmRpbmF0ZSByZWZlcmVuY2Ugc3lzdGVtKSBkZWZpbmVkCgpgYGB7ciB9CgogIGdncGxvdCgpICsgCiAgZ2VvbV9zZihkYXRhID0gcHVtYXNfMjAxMF9nZW9qc29uLAogICAgZmlsbCA9ICJncmV5IiwKICAgIGNvbG9yID0gImdyZXkiCiAgICApICsKICBnZW9tX3BvaW50KGRhdGEgPSBwdWJsaWNfbGlicmFyaWVzLCAKICAgICAgICAgICAgIGFlcyh4PWxvbmdpdHVkZSwKICAgICAgICAgICAgICAgICB5PWxhdGl0dWRlLAogICAgICAgICAgICAgICAgIGNvbG9yID0gb3ZlcmFnZW5jeSkpICsKICBzY2FsZV9jb2xvcl9tYW51YWwobmFtZSA9ICJTeXN0ZW0iLHZhbHVlcyA9IGxpYnJhcnlfc3lzdGVtX2NvbG9ycykgKwogICNhZGRpbmcgY29vcmQgc2YgZW5zdXJlcyBjcnMgbWF0Y2hlcwogIGNvb3JkX3NmKCkgKwogIHRoZW1lX3ZvaWQoKQoKCmBgYAoKIyMjIENobG9yb3BsZXRocwoKLSAgIE1hcCBhcmVhcyBjb2xvcmVkIGJ5IGRhdGEgdmFsdWUKCiMjIyMgQnJvYWRiYW5kIGFkb3B0aW9uIGluIE5ldyBZb3JrIENpdHkKCi0gICBOWUMgT3BlbiBEYXRhOiBbTllDJ3MgSW50ZXJuZXQgTWFzdGVyIFBsYW46IEhvbWUgQnJvYWRiYW5kIGFuZCBNb2JpbGUgQnJvYWRiYW5kIEFkb3B0aW9uIGJ5IFBVTUFdKGh0dHBzOi8vZGF0YS5jaXR5b2ZuZXd5b3JrLnVzL0NpdHktR292ZXJubWVudC9JbnRlcm5ldC1NYXN0ZXItUGxhbi1Ib21lLUJyb2FkYmFuZC1hbmQtTW9iaWxlLUJyby9nNWFoLWkyc2gpCgotICAgQmFzZWQgb24gQW1lcmljYW4gQ29tbXVuaXR5IFN1cnZleSBEYXRhOyBzaG93cyBzaGFyZSBvZiBob3VzZWhvbGRzIHRoYXQgaGF2ZSBib3RoIG1vYmlsZSBhbmQgaG9tZSBicm9hZGJhbmQKCmBgYHtyIHBhZ2VkLnByaW50PUZBTFNFLCBjYWNoZT1UUlVFfQoKYnJvYWRiYW5kX3VzZSA8LSByZWFkX2NzdigiaHR0cHM6Ly9kYXRhLmNpdHlvZm5ld3lvcmsudXMvYXBpL3ZpZXdzL2c1YWgtaTJzaC9yb3dzLmNzdj9hY2Nlc3NUeXBlPURPV05MT0FEIikKCgojY2xlYW4gdXAgdGhlIG5hbWVzIGEgYml0CmJyb2FkYmFuZF91c2UgPC0gYnJvYWRiYW5kX3VzZSAlPiUgCiAgcmVuYW1lKFBVTUEgPSBgUFVNQSAoUHVibGljIFVzZSBNaWNyb2RhdGEgU2FtcGxlIEFyZWFzKWAsCiAgICAgICAgIGJyb2FkYmFuZF9hZG9wdGlvbiA9IGBIb21lIEJyb2FkYmFuZCBhbmQgTW9iaWxlIEJyb2FkYmFuZCBBZG9wdGlvbiAoUGVyY2VudGFnZSBvZiAgSG91c2Vob2xkcylgLAogICAgICAgICBicm9hZGJhbmRfYWRvcHRpb25fcXVhcnRpbGUgPSBgSG9tZSBCcm9hZGJhbmQgYW5kIE1vYmlsZSBCcm9hZGJhbmQgQWRvcHRpb24gYnkgUXVhcnRpbGVzIChIaWdoLCBNZWRpdW0tSGlnaCwgTWVkaXVtLUxvdywgTG93KWApCgpicm9hZGJhbmRfdXNlICU+JSBoZWFkKCkKCgpgYGAKCi0gICBBZGQgdGhlIGJyb2FkYmFuZCBkYXRhIHRvIG91ciBQVU1BIGdlb20gZGF0YXNldAoKYGBge3IgY2FjaGU9VFJVRX0KCiNjb252ZXJ0IFBVTUEgdG8gc3RyaW5nOyBtYWtlIGFkb3B0aW9uIHF1YXJ0aWxlIGludG8gZmFjdG9yIHNvIGl0J2xsIHNvcnQgY29ycmVjdGx5ICAKYnJvYWRiYW5kX3VzZSA8LSBicm9hZGJhbmRfdXNlICU+JSAKICBtdXRhdGUoUFVNQSA9IGFzLmNoYXJhY3RlcihQVU1BKSwKICAgICAgICAgYnJvYWRiYW5kX2Fkb3B0aW9uX3F1YXJ0aWxlID0gZmFjdG9yKGJyb2FkYmFuZF9hZG9wdGlvbl9xdWFydGlsZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxldmVscyA9IGMoIkhpZ2giLCAiTWVkaXVtIEhpZ2giLCAiTWVkaXVtIExvdyIsICJMb3ciKSkpCgpwdW1hc18yMDEwX2dlb2pzb24gPC0gcHVtYXNfMjAxMF9nZW9qc29uICU+JSAKICBsZWZ0X2pvaW4oYnJvYWRiYW5kX3VzZSwgYnkgPSAiUFVNQSIpCgpwdW1hc18yMDEwX2dlb2pzb24gJT4lIHByaW50KCkKCgpgYGAKCi0gICBNYXAgdGhlIGBnZW9tX3NmYCBmaWxsIGFlc3RoZXRpYyB0byBicm9hZGJhbmRfYWRvcHRpb24KCi0gICBVc2Ugc2NhbGVfZmlsbF9ncmFkaWVudCB0byBzcGVjaWZ5IGZpbGwgY29sb3JzCgpgYGB7cn0KCiAgZ2dwbG90KCkgKyAKICBnZW9tX3NmKGRhdGEgPSBwdW1hc18yMDEwX2dlb2pzb24sCiAgICBhZXMoZmlsbCA9IGJyb2FkYmFuZF9hZG9wdGlvbiksCiAgICBjb2xvciA9ICJncmV5IgogICAgKSArCiAgZ2VvbV9wb2ludChkYXRhID0gcHVibGljX2xpYnJhcmllcywKICAgICAgICAgICAgIGFlcyh4PWxvbmdpdHVkZSwKICAgICAgICAgICAgICAgICB5PWxhdGl0dWRlLAogICAgICAgICAgICAgICAgIGNvbG9yID0gb3ZlcmFnZW5jeSksCiAgICAgICAgICAgICBzaXplID0gMikgKwogIHNjYWxlX2NvbG9yX21hbnVhbChuYW1lID0gIlN5c3RlbSIsdmFsdWVzID0gbGlicmFyeV9zeXN0ZW1fY29sb3JzKSArCiAgc2NhbGVfZmlsbF9ncmFkaWVudChsb3cgPSAiZ3JleTcwIixoaWdoID0gImdyZXkxMCIsIGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoYWNjdXJhY3kgPSAxKSwgbmFtZSA9ICJCcm9hZGJhbmQgYWRvcHRpb24iKSArCiAgY29vcmRfc2YoKSArCiAgdGhlbWVfdm9pZCgpCgpgYGAKCi0gICBPciBkZWZpbmUgZGlzY3JldGUgY29sb3JzIGFuZCB1c2UgYHNjYWxlX2ZpbGxfbWFudWFsYAoKYGBge3J9Cgpicm9hZGJhbmRfY29scyA8LSBjKCJncmV5MTAiLCJncmV5MzAiLCJncmV5NTAiLCJncmV5NzAiKSAlPiUgc2V0X25hbWVzKGxldmVscyhwdW1hc18yMDEwX2dlb2pzb24kYnJvYWRiYW5kX2Fkb3B0aW9uX3F1YXJ0aWxlKSkKCgpnZ3Bsb3QoKSArIAogIGdlb21fc2YoZGF0YSA9IHB1bWFzXzIwMTBfZ2VvanNvbiwKICAgIGFlcyhmaWxsID0gYnJvYWRiYW5kX2Fkb3B0aW9uX3F1YXJ0aWxlKSwKICAgIGNvbG9yID0gImdyZXkiCiAgICApICsKICBnZW9tX3BvaW50KGRhdGEgPSBwdWJsaWNfbGlicmFyaWVzLAogICAgICAgICAgICAgYWVzKHg9bG9uZ2l0dWRlLAogICAgICAgICAgICAgICAgIHk9bGF0aXR1ZGUsCiAgICAgICAgICAgICAgICAgY29sb3IgPSBvdmVyYWdlbmN5KSwKICAgICAgICAgICAgIHNpemUgPSAyKSArCiAgc2NhbGVfY29sb3JfbWFudWFsKG5hbWUgPSAiU3lzdGVtIix2YWx1ZXMgPSBsaWJyYXJ5X3N5c3RlbV9jb2xvcnMpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBicm9hZGJhbmRfY29scywgbmFtZSA9ICJCcm9hZGJhbmQgYWRvcHRpb24iKSArCiAgY29vcmRfc2YoKSArCiAgdGhlbWVfdm9pZCgpCgoKYGBgCgojIyMjIE1lZGlhbiBpbmNvbWUKCjxodHRwczovL3d3dzEubnljLmdvdi9zaXRlL3BsYW5uaW5nL3BsYW5uaW5nLWxldmVsL255Yy1wb3B1bGF0aW9uL2FtZXJpY2FuLWNvbW11bml0eS1zdXJ2ZXkucGFnZT4KCi0gICBEb3dubG9hZCBkYXRhIGFuZCBhZGQgaXQgdG8gb3VyIHB1bWFzXzIwMTBfZ2VvanNvbiBkYXRhIGZyYW1lCgpgYGB7ciBjYWNoZT1UUlVFfQoKCmRvd25sb2FkLmZpbGUodXJsID0gImh0dHBzOi8vd3d3MS5ueWMuZ292L2Fzc2V0cy9wbGFubmluZy9kb3dubG9hZC9vZmZpY2UvcGxhbm5pbmctbGV2ZWwvbnljLXBvcHVsYXRpb24vYWNzL2Vjb25fMjAxOF9hY3M1eXJfcHVtYS54bHN4IiwKICAgICAgICAgICAgICBkZXN0ZmlsZSA9ICJlY29uXzIwMThfYWNzNXlyX3B1bWEueGxzeCIpCgplY29uZGF0YV9wdW1hIDwtIHJlYWR4bDo6cmVhZF94bHN4KCJlY29uXzIwMThfYWNzNXlyX3B1bWEueGxzeCIsIHNoZWV0ID0gIkVjb25EYXRhIikKCnB1bWFzXzIwMTBfZ2VvanNvbiA8LSBwdW1hc18yMDEwX2dlb2pzb24gJT4lIAogIGxlZnRfam9pbihlY29uZGF0YV9wdW1hICU+JSAKICAgICAgICAgICAgICBzZWxlY3QoUFVNQSA9IEdlb0lELEdlb2dOYW1lLG1lZGlhbl9ob3VzZWhvbGRfaW5jb21lID0gTWRISEluY0UpICU+JSAKICAgICAgICAgICAgICBtdXRhdGUobWVkaWFuX2luY29tZV9jYXRlZ29yeSA9IAogICAgICAgICAgICAgICAgICAgICAgIGN1dChtZWRpYW5faG91c2Vob2xkX2luY29tZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgYnJlYWtzID0gYygwLDUwMDAwLDc1MDAwLDEwMDAwMCwxNTAwMDApLAogICAgICAgICAgICAgICAgICAgICAgICAgICBsYWJlbHMgPSBjKCIkMC01MEsiLCIkNTAtNzVLIiwiJDc1LTEwMEsiLCIkMTAwSysiKSkpKQoKCmBgYAoKLSAgIENyZWF0ZSBhIG5ldyBjb2xvciBzY2FsZSB3aXRoIHNvbWUgZ3JlZW5zCgpgYGB7cn0KbWVkaWFuX2luY29tZV9jYXRlZ29yeV9jb2xzIDwtIGMoIiNhOWNjYmMiLCIjN2ZiMjliIiwiIzRjN2Y2OCIsIiMzMzU1NDUiKSAlPiUgc2V0X25hbWVzKGxldmVscyhwdW1hc18yMDEwX2dlb2pzb24kbWVkaWFuX2luY29tZV9jYXRlZ29yeSkpCgpnZ3Bsb3QoKSArIAogIGdlb21fc2YoZGF0YSA9IHB1bWFzXzIwMTBfZ2VvanNvbiwKICAgIGFlcyhmaWxsID0gbWVkaWFuX2luY29tZV9jYXRlZ29yeSksCiAgICBjb2xvciA9ICJncmV5IgogICAgKSArCiAgZ2VvbV9wb2ludChkYXRhID0gcHVibGljX2xpYnJhcmllcywKICAgICAgICAgICAgIGFlcyh4PWxvbmdpdHVkZSwKICAgICAgICAgICAgICAgICB5PWxhdGl0dWRlLAogICAgICAgICAgICAgICAgIGNvbG9yID0gb3ZlcmFnZW5jeSksCiAgICAgICAgICAgICBzaXplID0gMikgKwogIHNjYWxlX2NvbG9yX21hbnVhbChuYW1lID0gIlN5c3RlbSIsdmFsdWVzID0gbGlicmFyeV9zeXN0ZW1fY29sb3JzKSArCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gbWVkaWFuX2luY29tZV9jYXRlZ29yeV9jb2xzLCBuYW1lID0gIk1lZGlhbiBpbmNvbWUiKSArCiAgY29vcmRfc2YoKSArCiAgdGhlbWVfdm9pZCgpIAoKYGBgCgojIExlYWZsZXQ6IGludGVyYWN0aXZlIG1hcHMKCmBgYHtyfQpsaWJyYXJ5KGxlYWZsZXQpCgpgYGAKCltMZWFmbGV0XShodHRwczovL2xlYWZsZXRqcy5jb20pOiBvcGVuIHNvdXJjZSBqYXZhc2NyaXB0IGxpYnJhcnkgZm9yIG1ha2luZyBpbnRlcmFjdGl2ZSBtYXBzIG9uIHZhcmlvdXMgcGxhdGZvcm1zCgpSIGhhcyBhIGBsZWFmbGV0YCBwYWNrYWdlIHRoYXQgbGV0cyB5b3UgY3JlYXRlIGxlYWZsZXQgIndpZGdldHMiIHdpdGhpbiBSCgpDcmVhdGUgYSBtYXAsIGFkZCB0aWxlcywgc2V0IHRoZSB2aWV3CgotICAgU2ltaWxhciB0byBnZ3Bsb3QgaW4gdGhhdCB3ZSBrZWVwIGFkZGluZyBsYXllcnMvZWxlbWVudHMsIGV4Y2VwdCB3ZSBjb250aW51ZSB1c2luZyB0aGUgbWFncml0dHIgcGlwZSBgJT4lYCB0byBhZGQgZWxlbWVudHMgdG8gYSBsZWFmbGV0IG1hcCAoaW5zdGVhZCBvZiB0aGUgZ2dwbG90IGArYCkKCi0gICBmb3Igbm93IHdlJ3JlIGNyZWF0aW5nIGEgbWFwIHdpdGhvdXQgYW55IGRhdGEKCmBgYHtyfQpsZWFmbGV0KCkgJT4lIAogICNhZGRUaWxlcygpICU+JSAKICBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAlPiUKICBzZXRWaWV3KGxhdCA9IDQwLjcsbG5nID0gLTc0LCB6b29tID0gMTApCgpgYGAKCkFkZCBvdXIgcHVibGljIGxpYnJhcnkgYnJhbmNoZXMKCi0gICBgYWRkQ2lyY2xlcygpYCBhZGRzIHBvaW50cyB0byB0aGUgbWFwCgotICAgY2FuIHNwZWNpZnkgdGhlIGRhdGEgc291cmNlIHdpdGhpbiB0aGUgYGFkZENpcmNsZXMoKWAgY2FsbCBvciBpbiB0aGUgb3JpZ2luYWwgYGxlYWZsZXQoKWAgY2FsbAoKLSAgIHRvIHJlZmVyIHRvIGZpZWxkcyBpbiBvdXIgZGF0YSBmcmFtZSwgdXNlIHRoZSB0aWxkZSBwcmVmaXggXH4KCmBgYHtyfQoKbGVhZmxldCgpICU+JQogIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICU+JQogIHNldFZpZXcobGF0ID0gNDAuNyxsbmcgPSAtNzQsIHpvb20gPSAxMCkgJT4lCiAgYWRkQ2lyY2xlcyhkYXRhID0gcHVibGljX2xpYnJhcmllcywKICAgICAgICAgICAgIGxuZyA9IH5sb25naXR1ZGUsCiAgICAgICAgICAgICBsYXQgPSB+bGF0aXR1ZGUKICAgICAgICAgICAgICkKCgpgYGAKCi0gICBVc2UgYGNvbG9yRmFjdG9yKClgIHRvIGNyZWF0ZSBhIHBhbGV0dGUgdG8gY29sb3IgdGhlIGNpcmNsZXMgYnkgc3lzdGVtCgotICAgVXNlIHRoZSBgbGFiZWxgIGFuZCBgcG9wdXBgIGFyZ3VtZW50cyB0byBhZGQgaG92ZXIvY2xpY2sgaW5mbwoKYGBge3J9CnBhbF9zeXN0ZW0gPC0gY29sb3JGYWN0b3IocGFsZXR0ZSA9IGxpYnJhcnlfc3lzdGVtX2NvbG9ycyAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgZG9tYWluID0gdW5pcXVlKHB1YmxpY19saWJyYXJpZXMkb3ZlcmFnZW5jeSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgb3JkZXJlZCA9IFQpCgpsZWFmbGV0KCkgJT4lCiAgYWRkUHJvdmlkZXJUaWxlcyhwcm92aWRlcnMkQ2FydG9EQi5Qb3NpdHJvbikgJT4lCiAgc2V0VmlldyhsYXQgPSA0MC43LGxuZyA9IC03NCwgem9vbSA9IDEwKSAlPiUKICBhZGRDaXJjbGVzKGRhdGEgPSBwdWJsaWNfbGlicmFyaWVzLAogICAgICAgICAgICAgbG5nID0gfmxvbmdpdHVkZSwKICAgICAgICAgICAgIGxhdCA9IH5sYXRpdHVkZSwKICAgICAgICAgICAgIGNvbG9yID0gfnBhbF9zeXN0ZW0ob3ZlcmFnZW5jeSksCiAgICAgICAgICAgICBmaWxsID0gfnBhbF9zeXN0ZW0ob3ZlcmFnZW5jeSksCiAgICAgICAgICAgICByYWRpdXMgPSAxMDAsCiAgICAgICAgICAgICBsYWJlbCA9IH5mYWNuYW1lLAogICAgICAgICAgICAgcG9wdXAgPSB+YWRkcmVzcwogICAgICAgICAgICAgKSAlPiUgCiAgYWRkTGVnZW5kKHBvc2l0aW9uID0gInRvcGxlZnQiLAogICAgICAgICAgICBwYWwgPSBwYWxfc3lzdGVtLCAKICAgICAgICAgICAgdmFsdWVzID0gdW5pcXVlKHB1YmxpY19saWJyYXJpZXMkb3ZlcmFnZW5jeSkpCgoKCgpgYGAKCkJlY2F1c2Ugd2UgaGF2ZSBhIGJhc2VtYXAsIHdlIGRvbid0IG5lZWQgdGhlIFBVTUEgcG9seWdvbnMgdG8gY3JlYXRlIGEgYmFzaWMgbWFwLiBCdXQgd2UgY2FuIGFkZCB0aGVtIGlmIHdlIHdhbnQgb3VyIGNobG9yb3BsZXRocyBvbiB0aGUgaW50ZXJhY3RpdmUgbWFwLgoKLSAgIEZpbGwgY29sb3IgYXJndW1lbnQgaW4gbGVhZmxldCBpcyBgZmlsbENvbG9yO2Agc3BlY2lmeSBvcGFjaXR5IHdpdGggYGZpbGxPcGFjaXR5YAoKLSAgIFVzZSBgc3Ryb2tlYCB0byBzcGVjaWZ5IHdoZXRoZXIgYm91bmRhcnkgbGluZXMgYXJlIGRyYXduOyBgY29sb3JgICwgYHdlaWdodGAsIGFuZCBgb3BhY2l0eWAgdG8gY29udHJvbCB0aGVpciBhcHBlYXJhbmNlCgpgYGB7cn0KCnBhbF9icm9hZGJhbmQgPC0gY29sb3JGYWN0b3IocGFsZXR0ZSA9IGJyb2FkYmFuZF9jb2xzLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkb21haW4gPSBsZXZlbHMocHVtYXNfMjAxMF9nZW9qc29uJGJyb2FkYmFuZF9hZG9wdGlvbl9xdWFydGlsZSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb3JkZXJlZCA9IFQpCgoKbGVhZmxldCgpICU+JQogIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICU+JQogIHNldFZpZXcobGF0ID0gNDAuNyxsbmcgPSAtNzQsIHpvb20gPSAxMCkgJT4lCiAgYWRkUG9seWdvbnMoZGF0YSA9IHB1bWFzXzIwMTBfZ2VvanNvbiwKICAgICAgICAgICAgICBmaWxsQ29sb3IgPSB+cGFsX2Jyb2FkYmFuZChicm9hZGJhbmRfYWRvcHRpb25fcXVhcnRpbGUpLAogICAgICAgICAgICAgIGZpbGxPcGFjaXR5ID0gLjcsCiAgICAgICAgICAgICAgY29sb3IgPSAiZ3JleSIsCiAgICAgICAgICAgICAgc3Ryb2tlID0gVCwKICAgICAgICAgICAgICB3ZWlnaHQgPSAxLAogICAgICAgICAgICAgIGxhYmVsID0gflBVTUEsCiAgICAgICAgICAgICAgcG9wdXAgPSB+cGFzdGUwKCI8Yj5QVU1BICIsUFVNQSwiPC9iPiIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIjxicj4iLCBHZW9nTmFtZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiPGJyPkJyb2FkYmFuZCBhZG9wdGlvbjogIixyb3VuZChicm9hZGJhbmRfYWRvcHRpb24qMTAwLDApLCIlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiPGJyPkJyb2FkYmFuZCBhZG9wdGlvbiBjYXRlZ29yeTogIixicm9hZGJhbmRfYWRvcHRpb25fcXVhcnRpbGUpKSAKCgpgYGAKCkNvbWJpbmUgaXQgd2l0aCB0aGUgbGlicmFyaWVzIGFuZCBhZGQgYSBsZWdlbmQKCi0gICBPcmRlciBtYXR0ZXJzIC0gZWFjaCBzdWNjZXNzaXZlIGxheWVyIGlzIHBsb3R0ZWQgb24gdG9wIG9mIHRoZSBwcmV2aW91cyBvbmVzCgpgYGB7cn0KCmxlYWZsZXQoKSAlPiUKICBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAlPiUKICBzZXRWaWV3KGxhdCA9IDQwLjcsbG5nID0gLTc0LCB6b29tID0gMTApICU+JQogIGFkZFBvbHlnb25zKGRhdGEgPSBwdW1hc18yMDEwX2dlb2pzb24sCiAgICAgICAgICAgICAgZmlsbENvbG9yID0gfnBhbF9icm9hZGJhbmQoYnJvYWRiYW5kX2Fkb3B0aW9uX3F1YXJ0aWxlKSwKICAgICAgICAgICAgICBmaWxsT3BhY2l0eSA9IC43LAogICAgICAgICAgICAgIGNvbG9yID0gImdyZXkiLAogICAgICAgICAgICAgIHN0cm9rZSA9IFQsCiAgICAgICAgICAgICAgd2VpZ2h0ID0gMSwKICAgICAgICAgICAgICBsYWJlbCA9IH5QVU1BLAogICAgICAgICAgICAgIHBvcHVwID0gfnBhc3RlMCgiPGI+UFVNQSAiLFBVTUEsIjwvYj4iLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiPGJyPiIsIEdlb2dOYW1lLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICI8YnI+QnJvYWRiYW5kIGFkb3B0aW9uOiAiLHJvdW5kKGJyb2FkYmFuZF9hZG9wdGlvbioxMDAsMCksIiUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICI8YnI+QnJvYWRiYW5kIGFkb3B0aW9uIGNhdGVnb3J5OiAiLGJyb2FkYmFuZF9hZG9wdGlvbl9xdWFydGlsZSkpICU+JSAKICAgIGFkZENpcmNsZXMoZGF0YSA9IHB1YmxpY19saWJyYXJpZXMsCiAgICAgICAgICAgICBsbmcgPSB+bG9uZ2l0dWRlLAogICAgICAgICAgICAgbGF0ID0gfmxhdGl0dWRlLAogICAgICAgICAgICAgY29sb3IgPSB+cGFsX3N5c3RlbShvdmVyYWdlbmN5KSwKICAgICAgICAgICAgIGZpbGwgPSB+cGFsX3N5c3RlbShvdmVyYWdlbmN5KSwKICAgICAgICAgICAgIHJhZGl1cyA9IDEwMCwKICAgICAgICAgICAgIGxhYmVsID0gfmZhY25hbWUsCiAgICAgICAgICAgICBwb3B1cCA9IH5hZGRyZXNzCiAgICAgICAgICAgICApICU+JSAKICBhZGRMZWdlbmQocG9zaXRpb24gPSAidG9wbGVmdCIsCiAgICAgICAgICAgIHBhbCA9IHBhbF9zeXN0ZW0sIAogICAgICAgICAgICB2YWx1ZXMgPSBwdWJsaWNfbGlicmFyaWVzJG92ZXJhZ2VuY3ksCiAgICAgICAgICAgIHRpdGxlID0gIkxpYnJhcnkgU3lzdGVtIikgJT4lIAogIGFkZExlZ2VuZChwb3NpdGlvbiA9ICJ0b3BsZWZ0IiwKICAgICAgICAgICAgcGFsID0gcGFsX2Jyb2FkYmFuZCwgCiAgICAgICAgICAgIHZhbHVlcyA9ICBwdW1hc18yMDEwX2dlb2pzb24kYnJvYWRiYW5kX2Fkb3B0aW9uX3F1YXJ0aWxlLAogICAgICAgICAgICBvcGFjaXR5ID0gLjcsCiAgICAgICAgICAgIHRpdGxlID0gIkJyb2FkYmFuZCBBY2Nlc3MiKQoKYGBgCgpWaXN1YWxpemUgbW9yZSB0aGFuIG9uZSBsYXllcgoKLSAgIFVzZSBgZ3JvdXBgIGFyZ3VtZW50IHdpdGhpbiBlYWNoIGxheWVyIHRvIHNwZWNpZnkgd2hpY2ggZ3JvdXAgaXQgYmVsb25ncyB0bwoKLSAgIGBhZGRMYXllcnNDb250cm9sYCBhZGRzIGEgY29udHJvbCB0byB0b2dnbGUgYmV0d2VlbiBsYXllcnMgb3IgdHVybiBsYXllcnMgb24gYW5kIG9mZgoKLSAgIGFzc2lnbmluZyB0aGUgbWFwIHRvIGFuIG9iamVjdCBjcmVhdGVzIGEgbGVhZmxldCBvYmplY3Qgd2l0aCBjbGFzcyBgaHRtbHdpZGdldGAKCmBgYHtyfQpwYWxfaW5jb21lIDwtIGNvbG9yRmFjdG9yKHBhbGV0dGUgPSBtZWRpYW5faW5jb21lX2NhdGVnb3J5X2NvbHMsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRvbWFpbiA9IGxldmVscyhwdW1hc18yMDEwX2dlb2pzb24kbWVkaWFuX2luY29tZV9jYXRlZ29yeSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb3JkZXJlZCA9IFQpCgpsaWJyYXJpZXNfbWFwIDwtIGxlYWZsZXQoKSAlPiUKICBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAlPiUKICBzZXRWaWV3KGxhdCA9IDQwLjcsbG5nID0gLTc0LCB6b29tID0gMTApICU+JQogIGFkZFBvbHlnb25zKGRhdGEgPSBwdW1hc18yMDEwX2dlb2pzb24sCiAgICAgICAgICAgICAgZmlsbENvbG9yID0gfnBhbF9icm9hZGJhbmQoYnJvYWRiYW5kX2Fkb3B0aW9uX3F1YXJ0aWxlKSwKICAgICAgICAgICAgICBmaWxsT3BhY2l0eSA9IC43LAogICAgICAgICAgICAgIGNvbG9yID0gImdyZXkiLAogICAgICAgICAgICAgIHN0cm9rZSA9IFQsCiAgICAgICAgICAgICAgd2VpZ2h0ID0gMSwKICAgICAgICAgICAgICBsYWJlbCA9IH5QVU1BLAogICAgICAgICAgICAgIHBvcHVwID0gfnBhc3RlMCgiPGI+UFVNQSAiLFBVTUEsIjwvYj4iLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiPGJyPiIsIEdlb2dOYW1lLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICI8YnI+QnJvYWRiYW5kIGFkb3B0aW9uOiAiLHJvdW5kKGJyb2FkYmFuZF9hZG9wdGlvbioxMDAsMCksIiUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICI8YnI+QnJvYWRiYW5kIGFkb3B0aW9uIGNhdGVnb3J5OiAiLGJyb2FkYmFuZF9hZG9wdGlvbl9xdWFydGlsZSksCiAgICAgICAgICAgICAgZ3JvdXAgPSAiQnJvYWRiYW5kIikgJT4lIAogIGFkZFBvbHlnb25zKGRhdGEgPSBwdW1hc18yMDEwX2dlb2pzb24sCiAgICAgICAgICAgICAgZmlsbENvbG9yID0gfnBhbF9pbmNvbWUobWVkaWFuX2luY29tZV9jYXRlZ29yeSksCiAgICAgICAgICAgICAgZmlsbE9wYWNpdHkgPSAuNywKICAgICAgICAgICAgICBjb2xvciA9ICJncmV5IiwKICAgICAgICAgICAgICBzdHJva2UgPSBULAogICAgICAgICAgICAgIHdlaWdodCA9IDEsCiAgICAgICAgICAgICAgbGFiZWwgPSB+UFVNQSwKICAgICAgICAgICAgICBwb3B1cCA9IH5wYXN0ZTAoIjxiPlBVTUEgIixQVU1BLCI8L2I+IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIjxicj4iLCBHZW9nTmFtZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiPGJyPk1lZGlhbiBpbmNvbWU6ICQiLGZvcm1hdChtZWRpYW5faG91c2Vob2xkX2luY29tZSxiaWcubWFyayA9ICIsIiksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIjxicj5NZWRpYW4gaW5jb21lIGNhdGVnb3J5OiAiLG1lZGlhbl9pbmNvbWVfY2F0ZWdvcnkpLAogICAgICAgICAgICAgIGdyb3VwID0gImluY29tZSIpICU+JSAKICAgIGFkZENpcmNsZXMoZGF0YSA9IHB1YmxpY19saWJyYXJpZXMsCiAgICAgICAgICAgICBsbmcgPSB+bG9uZ2l0dWRlLAogICAgICAgICAgICAgbGF0ID0gfmxhdGl0dWRlLAogICAgICAgICAgICAgY29sb3IgPSB+cGFsX3N5c3RlbShvdmVyYWdlbmN5KSwKICAgICAgICAgICAgIGZpbGwgPSB+cGFsX3N5c3RlbShvdmVyYWdlbmN5KSwKICAgICAgICAgICAgIG9wYWNpdHkgPSAxLAogICAgICAgICAgICAgZmlsbE9wYWNpdHkgPSAxLAogICAgICAgICAgICAgcmFkaXVzID0gMTAwLAogICAgICAgICAgICAgbGFiZWwgPSB+ZmFjbmFtZSwKICAgICAgICAgICAgIHBvcHVwID0gfmFkZHJlc3MjLAogICAgICAgICAgICAgI2dyb3VwID0gIkJyb2FkYmFuZCIKICAgICAgICAgICAgICkgJT4lIAogICAgIyBhZGRDaXJjbGVzKGRhdGEgPSBwdWJsaWNfbGlicmFyaWVzLAogICAgIyAgICAgICAgICBsbmcgPSB+bG9uZ2l0dWRlLAogICAgIyAgICAgICAgICBsYXQgPSB+bGF0aXR1ZGUsCiAgICAjICAgICAgICAgIGNvbG9yID0gfnBhbF9zeXN0ZW0ob3ZlcmFnZW5jeSksCiAgICAjICAgICAgICAgIGZpbGwgPSB+cGFsX3N5c3RlbShvdmVyYWdlbmN5KSwKICAgICMgICAgICAgICAgb3BhY2l0eSA9IDEsCiAgICAjICAgICAgICAgIGZpbGxPcGFjaXR5ID0gMSwKICAgICMgICAgICAgICAgcmFkaXVzID0gMTAwLAogICAgIyAgICAgICAgICBsYWJlbCA9IH5mYWNuYW1lLAogICAgIyAgICAgICAgICBwb3B1cCA9IH5hZGRyZXNzLAogICAgIyAgICAgICAgICBncm91cCA9ICJJbmNvbWUiCiAgICAjICAgICAgICAgICkgJT4lIAogIGFkZExlZ2VuZChwb3NpdGlvbiA9ICJ0b3BsZWZ0IiwKICAgICAgICAgICAgcGFsID0gcGFsX3N5c3RlbSwgCiAgICAgICAgICAgIHZhbHVlcyA9IHB1YmxpY19saWJyYXJpZXMkb3ZlcmFnZW5jeSwKICAgICAgICAgICAgdGl0bGUgPSAiTGlicmFyeSBTeXN0ZW0iKSAlPiUgCiAgYWRkTGVnZW5kKHBvc2l0aW9uID0gInRvcGxlZnQiLAogICAgICAgICAgICBwYWwgPSBwYWxfYnJvYWRiYW5kLCAKICAgICAgICAgICAgdmFsdWVzID0gIHB1bWFzXzIwMTBfZ2VvanNvbiRicm9hZGJhbmRfYWRvcHRpb25fcXVhcnRpbGUsCiAgICAgICAgICAgIG9wYWNpdHkgPSAuNywKICAgICAgICAgICAgdGl0bGUgPSAiQnJvYWRiYW5kIEFjY2VzcyIsCiAgICAgICAgICAgIGdyb3VwID0gIkJyb2FkYmFuZCIpICU+JSAKICBhZGRMZWdlbmQocG9zaXRpb24gPSAidG9wbGVmdCIsCiAgICAgICAgICAgIHBhbCA9IHBhbF9pbmNvbWUsIAogICAgICAgICAgICB2YWx1ZXMgPSAgcHVtYXNfMjAxMF9nZW9qc29uJG1lZGlhbl9pbmNvbWVfY2F0ZWdvcnksCiAgICAgICAgICAgIG9wYWNpdHkgPSAuNywKICAgICAgICAgICAgdGl0bGUgPSAiTWVkaWFuIGhvdXNlaG9sZCBpbmNvbWUiLAogICAgICAgICAgICBncm91cCA9ICJJbmNvbWUiKSAlPiUgCiAgYWRkTGF5ZXJzQ29udHJvbCgKICAgICAgICAgICAgYmFzZUdyb3VwcyA9IGMoIkJyb2FkYmFuZCIsIkluY29tZSIpLAogICAgICAgICAgICBvcHRpb25zID0gbGF5ZXJzQ29udHJvbE9wdGlvbnMoY29sbGFwc2VkID0gRikjLAogICAgICAgICAgICAjb3ZlcmxheUdyb3VwcyA9IGMoIkJyYW5jaGVzIikKICApIAoKCmxpYnJhcmllc19tYXAKCmBgYAoKVGhlIGh0bWx3aWRnZXRzIHBhY2thZ2UgbGV0cyB5b3Ugc2F2ZSB0aGlzIG1hcCBhcyBhbiBodG1sIGZpbGUsIHdoaWNoIGNhbiB0aGVuIGJlIG9wZW5lZCBpbiBhIGJyb3dzZXIgb3IgZW1iZWRkZWQgaW4gYSB3ZWIgcGFnZQoKYGBge3J9Cmh0bWx3aWRnZXRzOjpzYXZlV2lkZ2V0KGxpYnJhcmllc19tYXAsCiAgICAgICAgICAgICAgICAgICAgICAgIGZpbGUgPSAibGlicmFyaWVzX21hcC5odG1sIiwKICAgICAgICAgICAgICAgICAgICAgICAgdGl0bGUgPSAiTllDIExpYnJhcmllczogIEJyb2FkYmFuZCBBY2Nlc3MgYW5kIE1lZGlhbiBJbmNvbWUiKQoKYGBgCgojIyBPdGhlciByZXNvdXJjZXMKCi0gICBbYHRpZHljZW5zdXNgXShodHRwczovL3dhbGtlci1kYXRhLmNvbS90aWR5Y2Vuc3VzLykgcGFja2FnZTogYW1hemluZyBSIGludGVyZmFjZSBmb3IgQ2Vuc3VzL0FDUyBBUEksIGluY2x1ZGluZyBnZW9tZXRyaWVzCgogICAgLSAgIFtBbmFseXppbmcgQ2Vuc3VzIERhdGE6IE1ldGhvZHMgTWFwcyBhbmQgTW9kZWxzIGluIFJdKGh0dHBzOi8vd2Fsa2VyLWRhdGEuY29tL2NlbnN1cy1yLykKCi0gICBbYHRpZ3Jpc2BdKGh0dHBzOi8vZ2l0aHViLmNvbS93YWxrZXJrZS90aWdyaXMpIHBhY2thZ2U6IG1vcmUgQ2Vuc3VzIGdlb21ldHJpZXMKCi0gICBbYHJuYXR1cmFsZWFydGhgXShodHRwczovL2RvY3Mucm9wZW5zY2kub3JnL3JuYXR1cmFsZWFydGgvKTogd29ybGQgbWFwIGRhdGEKCi0gICBbYHRtYXBgXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvdG1hcC92aWduZXR0ZXMvdG1hcC1nZXRzdGFydGVkLmh0bWwpOiBBbHRlcm5hdGl2ZSBwYWNrYWdlIGZvciB0bWFwcGluZyBpbiBSLCB3aXRoIGdncGxvdC1saWtlIHN5bnRheCAtIHNvbWUgZmluZCB0aGlzIHRoZSBiZXN0IHdheSB0byBnZXQgc3RhcnRlZAoKLSAgIFtgbWFwYm94YXBpYF0oaHR0cHM6Ly93YWxrZXItZGF0YS5jb20vbWFwYm94YXBpLykgcGFja2FnZTogbW9yZSBiYXNlbWFwIG9wdGlvbnMsIGdlb2NvZGluZyBzZXJ2aWNlcywgaXNvY2hyb25lcyAocmVxdWlyZXMgc2V0dGluZyB1cCBhIG1hcGJveCBhY2NvdW50KQoKLSAgIEdlb2NvZGluZwoKICAgIC0gICBXaXRoaW4gTllDLCBbR2Vvc3VwcG9ydF0oaHR0cHM6Ly93d3cxLm55Yy5nb3Yvc2l0ZS9wbGFubmluZy9kYXRhLW1hcHMvb3Blbi1kYXRhL2R3bi1nZGUtaG9tZS5wYWdlKQoKICAgIC0gICBbVXJiYW4gSW5zdGl0dXRlIG92ZXJ2aWV3IG9mIG90aGVyIHRvb2xzXShodHRwczovL3VyYmFuLWluc3RpdHV0ZS5tZWRpdW0uY29tL2Nob29zaW5nLWEtZ2VvY29kZXItZm9yLXRoZS11cmJhbi1pbnN0aXR1dGUtODYxOTJmNjU2YzVmKQoKLSAgIFtHZW9jb21wdXRhdGlvbiB3aXRoIFJdKGh0dHBzOi8vZ2VvY29tcHIucm9iaW5sb3ZlbGFjZS5uZXQvaW5kZXguaHRtbCkK